package jam4j.lang;

import java.util.*;

final class Modifiers {
    private EnumSet<FilePath.Part> selections;
    private EnumMap<FilePath.Part, Expression> alterations;
    private boolean parent, downshift, upshift;
    private Expression empty, join;
    private Expression start, end, index;
    
    Modifiers(EnumSet<FilePath.Part> selections,
            EnumMap<FilePath.Part, Expression> alterations,
            boolean parent, boolean downshift, boolean upshift,
            Expression start, Expression end, Expression index,
            Expression empty, Expression join) {
        assert start == null ? end == null : index == null;
        
        this.selections = selections;
        this.alterations = alterations;
        this.parent = parent;
        this.downshift = downshift;
        this.upshift = upshift;
        this.start = start;
        this.end = end;
        this.index = index;
        this.empty = empty;
        this.join = join;
    }
    
    Modifiers() { }
    
    void select(FilePath.Part part) {
        if (selections == null)
            selections = EnumSet.of(part);
        else
            selections.add(part);
    }
    
    void alter(FilePath.Part part, Expression newValue) {
        if (alterations == null)
            alterations = new EnumMap<FilePath.Part, Expression>(
                    FilePath.Part.class);
        alterations.put(part, newValue);
        
        if (selections != null)
            selections.add(part);
    }
    
    void selectParent() { parent = true; }
    void downshift() { downshift = true; }
    void upshift() { upshift = true; }
    
    void setIndex(Expression index) {
        this.index = index;
        start = end = null;
    }
    
    void setInterval(Expression start, Expression end) {
        this.start = start;
        this.end = end;
        index = null;
    }
    
    void setDefault(Expression empty) {
        this.empty = empty;
    }
    
    void joinWith(Expression delim) {
        this.join = delim;
    }
    
    private static int[] interval(int start, int end) {
        int[] ans = new int[end - start];
        int ix = 0;
        while (start < end)
            ans[ix++] = start++;
        return ans;
    }
    
    private static int[] indices(Expression value, Context cxt) {
        final String[] strs = value.resolve(cxt);
        final int[] ans = new int[strs.length];
        for (int ix = 0; ix < strs.length; ix++)
            ans[ix] = Integer.valueOf(strs[ix]) - 1;
        return ans;
    }
    
    private static int[][] eachOnce(int count) {
        return new int[][] { interval(0, count) };
    }
    
    String[] modify(String[] values, Context cxt) {
        final List<String> ans = new ArrayList<String>();
        
        if (values.length == 0 && empty != null)
            values = empty.resolve(cxt);
        final int valueCount = values.length;
        
        final int[][] ixTable;
        BUILD_TABLE: {
            if (start == null)
                if (index == null)
                    // No subscripting
                    ixTable = eachOnce(valueCount);
                else {
                    final int[] ixs;
                    try {
                        ixs = indices(index, cxt);
                    } catch (NumberFormatException e) {
                        ixTable = eachOnce(valueCount);
                        break BUILD_TABLE;
                    }
                    
                    ixTable = new int[][] { ixs };
                }
            else {
                final int[] startIxs, endIxs;
                
                try {
                    startIxs = indices(start, cxt);
                } catch (NumberFormatException e) {
                    ixTable = eachOnce(valueCount);
                    break BUILD_TABLE;
                }
                
                if (end == null)
                    endIxs = new int[] { valueCount - 1 };
                else
                    try {
                        endIxs = indices(start, cxt);
                    } catch (NumberFormatException e) {
                        ixTable = eachOnce(valueCount);
                        break BUILD_TABLE;
                    }
                
                ixTable = new int[startIxs.length * endIxs.length][];
                int rowIx = 0;
                for (int startIx : startIxs)
                    for (int endIx : endIxs)
                        ixTable[rowIx++] = interval(startIx, endIx + 1);
            }
        }
        
        for (int[] ixs : ixTable)
            for (int ix : ixs)
                if (ix < valueCount)
                    modify(values[ix], cxt, ans);
        
        if (join != null)
            return multijoin(join.resolve(cxt), ans);
        
        return ans.toArray(new String[ans.size()]);
    }
    
    private void add(String string, List<String> acc) {
        if (upshift)
            acc.add(string.toUpperCase());
        else if (downshift)
            acc.add(string.toLowerCase());
        else
            acc.add(string);
    }
    
    private void modify(String string, Context cxt, List<String> acc) {
        modify(FilePath.parse(string), cxt, acc);
    }

    private void modify(FilePath filePath, Context cxt, List<String> acc) {
        final FilePath[] altered = filePath.modify(alterations, cxt);
        for (FilePath path : altered) {
            path.select(selections);
            if (parent)
                path.parent();
            add(path.toString(), acc);
        }
    }
    
    private static String[] multijoin(String[] delims, List<String> values) {
        final StringBuilder builder = new StringBuilder();
        
        final int delimCount;
        if ((delimCount = delims.length) == 0 || values.isEmpty())
            return Expression.UNDEF;
        
        for (int delimIx = 0;;) {
            final String delim = delims[delimIx];
            
            for (Iterator<String> valueIter = values.iterator();;) {
                builder.append(valueIter.next());
                
                if (valueIter.hasNext())
                    builder.append(delim);
                else
                    break;
            }
            
            if (++delimIx == delimCount)
                break;
            
            builder.append(delim);
        }
        
        return new String[] { builder.toString() };
    }
    
    private String subscript() {
        if (index != null)
            return "[" + index + ']';
        else if (start != null)
            if (end != null)
                return "[" + start + "-" + end + "]";
            else
                return "[" + start + "-]";
        else
            return "";
    }
    
    private boolean hasSelections() {
        return selections != null && !selections.isEmpty();
    }
    
    private boolean hasAlterations() {
        return alterations != null && !alterations.isEmpty();
    }
    
    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder(subscript());
        if (hasSelections() || parent || downshift || upshift) {
            builder.append(':');
            if (hasSelections())
                for (FilePath.Part part : selections)
                    builder.append(partChar(part));
            if (parent)
                builder.append('P');
            if (downshift)
                builder.append('D');
            if (upshift)
                builder.append('U');
        }
        
        if (hasAlterations())
            for (Map.Entry<FilePath.Part, Expression> entry :
                    alterations.entrySet())
                builder
                    .append(':').append(partChar(entry.getKey()))
                    .append('=').append(entry.getValue());
        
        if (empty != null)
            builder.append(":E=").append(empty);
        if (join != null)
            builder.append(":J=").append(join);
        
        return builder.toString();
    }
    
    private char partChar(FilePath.Part part) {
        switch (part) {
            case GRIST:         return 'G';
            case ROOT:          return 'R';
            case DIR:           return 'D';
            case BASE:          return 'B';
            case SUFFIX:        return 'S';
            case MEMBER:        return 'M';
            default:            return '?';
        }
    }
}